-
Notifications
You must be signed in to change notification settings - Fork 529
fix: improve cctp v2 offchain lookup service #7334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
📝 WalkthroughWalkthroughThis pull request adds CCTP v2 support with PENDING attestation detection and error handling in the attestation service, while refactoring CCIP-read metadata fetching to use persistent response tracking and improved multi-URL retry logic with explicit error handling. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant CCTPService as CCTP Service
participant Attestation as Attestation Endpoint
participant Prometheus
Client->>CCTPService: getAttestation(txHash)
CCTPService->>Attestation: fetch attestation JSON
Attestation-->>CCTPService: CCTPData with messages[]
loop Iterate Messages
CCTPService->>CCTPService: Check message status
alt Status is PENDING
CCTPService->>Prometheus: Log unhandled error (if known reason)
CCTPService->>Client: Throw descriptive error
else Status is other
CCTPService->>Client: Return attestation
end
end
sequenceDiagram
participant Client
participant CCIPRead as CCIP-read Metadata
participant URLs as URL List
Client->>CCIPRead: Fetch metadata
loop For each URL
CCIPRead->>URLs: Attempt fetch (data-inlined or POST)
alt Fetch succeeds & response ok
CCIPRead->>CCIPRead: Parse responseJson
CCIPRead->>Client: Return ensure0x(responseJson.data)
else Fetch fails or response not ok
CCIPRead->>CCIPRead: Log warning
Note over CCIPRead: Continue to next URL
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
typescript/ccip-server/src/services/CCTPAttestationService.ts (3)
16-20: Consider makingeventNonceoptional for consistency.Since
eventNonceis a CCTP v2 field like the others, it should probably be optional too. This would better align with the backward compatibility pattern you've got going here.Apply this diff:
interface CCTPMessageEntry { attestation: string; message: string; - eventNonce: string; + eventNonce?: string; // CCTP v2 only cctpVersion?: string; status?: Status; delayReason?: DelayReason; }
166-178: Consider adjusting log level for expected pending states.Looking at this, you're logging all PENDING states as errors, even the ones that are just... well, pending normally. Things like
pending_confirmationsare expected and temporary - they'll sort themselves out. The serious stuff (likeinsufficient_fee) deserves the error level, but the rest? Maybeinfoorwarnwould fit better.Based on the past review discussion, I know you want to keep things simple and tune alerts separately, but having the right log level from the start makes life easier down the road.
Here's one way to handle it:
json.messages.forEach((message) => { if (message.attestation === 'PENDING') { const errorString = message.delayReason ? `CCTP attestation is pending due to ${message.delayReason}` : 'CCTP attestation is still pending'; switch (message.delayReason) { case 'insufficient_fee': case 'amount_above_max': case 'insufficient_allowance_available': PrometheusMetrics.logUnhandledError(this.serviceName); + logger.error(context, errorString); break; + default: + logger.info(context, errorString); + break; } - logger.error(context, errorString); throw new Error(errorString); } });
166-181: TheforEachloop checks all messages but only the first is returned.You've got this loop checking every message for PENDING states, but then line 181 just grabs the first one and calls it a day. If there's only ever one message per transaction (which the TODO suggests), might be cleaner to just work with
json.messages[0]directly. Otherwise, if there could be multiple messages, you'd want to track which one you're actually returning.If single message is expected:
- json.messages.forEach((message) => { - if (message.attestation === 'PENDING') { + const message = json.messages[0]; + if (message.attestation === 'PENDING') { // ... existing error handling - } - }); + } // TODO: handle multiple messages in one tx hash - return [json.messages[0].message, json.messages[0].attestation]; + return [message.message, message.attestation];
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
typescript/ccip-server/src/services/CCTPAttestationService.ts(2 hunks)typescript/sdk/src/ism/metadata/ccipread.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
typescript/sdk/src/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Keep TypeScript SDK source under typescript/sdk/src/ using the documented abstractions (MultiProvider, HyperlaneCore, MultiProtocolCore)
Files:
typescript/sdk/src/ism/metadata/ccipread.ts
🧬 Code graph analysis (1)
typescript/ccip-server/src/services/CCTPAttestationService.ts (1)
typescript/ccip-server/src/utils/prometheus.ts (1)
PrometheusMetrics(22-29)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (53)
- GitHub Check: cli-evm-e2e-matrix (core-read)
- GitHub Check: cli-evm-e2e-matrix (warp-rebalancer)
- GitHub Check: cli-evm-e2e-matrix (warp-check-4)
- GitHub Check: cli-evm-e2e-matrix (warp-extend-recovery)
- GitHub Check: cli-evm-e2e-matrix (warp-read)
- GitHub Check: cli-evm-e2e-matrix (warp-send)
- GitHub Check: cli-evm-e2e-matrix (warp-extend-basic)
- GitHub Check: cli-evm-e2e-matrix (warp-extend-config)
- GitHub Check: cli-evm-e2e-matrix (warp-init)
- GitHub Check: cli-evm-e2e-matrix (core-check)
- GitHub Check: cli-evm-e2e-matrix (relay)
- GitHub Check: cli-evm-e2e-matrix (warp-bridge-1)
- GitHub Check: cli-evm-e2e-matrix (warp-check-5)
- GitHub Check: cli-evm-e2e-matrix (warp-check-3)
- GitHub Check: cli-evm-e2e-matrix (warp-deploy-2)
- GitHub Check: cli-evm-e2e-matrix (warp-deploy-1)
- GitHub Check: cli-evm-e2e-matrix (warp-apply-2)
- GitHub Check: cli-evm-e2e-matrix (warp-check-2)
- GitHub Check: cli-evm-e2e-matrix (warp-bridge-2)
- GitHub Check: cli-evm-e2e-matrix (warp-apply-submitters)
- GitHub Check: cli-evm-e2e-matrix (warp-check-1)
- GitHub Check: cli-evm-e2e-matrix (warp-apply-1)
- GitHub Check: cli-evm-e2e-matrix (core-apply)
- GitHub Check: cli-evm-e2e-matrix (core-deploy)
- GitHub Check: cli-evm-e2e-matrix (warp-apply-ism-updates)
- GitHub Check: cli-evm-e2e-matrix (core-init)
- GitHub Check: env-test-matrix (mainnet3, optimism, igp)
- GitHub Check: env-test-matrix (mainnet3, optimism, core)
- GitHub Check: env-test-matrix (mainnet3, inevm, igp)
- GitHub Check: env-test-matrix (mainnet3, ethereum, igp)
- GitHub Check: env-test-matrix (mainnet3, inevm, core)
- GitHub Check: env-test-matrix (mainnet3, arbitrum, igp)
- GitHub Check: env-test-matrix (testnet4, sepolia, core)
- GitHub Check: cli-cosmos-e2e-matrix (warp-read)
- GitHub Check: env-test-matrix (mainnet3, ethereum, core)
- GitHub Check: env-test-matrix (mainnet3, arbitrum, core)
- GitHub Check: cli-cosmos-e2e-matrix (core-check)
- GitHub Check: cli-cosmos-e2e-matrix (warp-deploy)
- GitHub Check: cli-cosmos-e2e-matrix (core-read)
- GitHub Check: cli-cosmos-e2e-matrix (core-apply)
- GitHub Check: cli-cosmos-e2e-matrix (core-deploy)
- GitHub Check: cosmos-sdk-e2e-run
- GitHub Check: cli-cross-chain-e2e-matrix (warp-apply)
- GitHub Check: cli-cross-chain-e2e-matrix (warp-deploy)
- GitHub Check: cli-radix-e2e-matrix (core-deploy)
- GitHub Check: yarn-test-run
- GitHub Check: cli-install-test-run
- GitHub Check: build-and-push-to-gcr
- GitHub Check: lint-rs
- GitHub Check: lander-coverage
- GitHub Check: test-rs
- GitHub Check: e2e-matrix (evm)
- GitHub Check: agent-configs (testnet4)
| json.messages.forEach((message) => { | ||
| if (message.attestation === 'PENDING') { | ||
| const errorString = `CCTP attestation is pending due to ${message.delayReason}`; | ||
| switch (message.delayReason) { | ||
| case 'insufficient_fee': | ||
| case 'amount_above_max': | ||
| case 'insufficient_allowance_available': | ||
| PrometheusMetrics.logUnhandledError(this.serviceName); | ||
| } | ||
| logger.error(context, errorString); | ||
| throw new Error(errorString); | ||
| } | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle undefined delayReason to avoid confusing error messages.
Right now, if delayReason is undefined (which it can be for normal pending states), you'll get an error message like "CCTP attestation is pending due to undefined". Not exactly helpful when you're trying to figure out what went wrong.
Consider this approach:
json.messages.forEach((message) => {
if (message.attestation === 'PENDING') {
- const errorString = `CCTP attestation is pending due to ${message.delayReason}`;
+ const errorString = message.delayReason
+ ? `CCTP attestation is pending due to ${message.delayReason}`
+ : 'CCTP attestation is still pending';
switch (message.delayReason) {
case 'insufficient_fee':
case 'amount_above_max':
case 'insufficient_allowance_available':
PrometheusMetrics.logUnhandledError(this.serviceName);
+ break;
}
logger.error(context, errorString);
throw new Error(errorString);
}
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| json.messages.forEach((message) => { | |
| if (message.attestation === 'PENDING') { | |
| const errorString = `CCTP attestation is pending due to ${message.delayReason}`; | |
| switch (message.delayReason) { | |
| case 'insufficient_fee': | |
| case 'amount_above_max': | |
| case 'insufficient_allowance_available': | |
| PrometheusMetrics.logUnhandledError(this.serviceName); | |
| } | |
| logger.error(context, errorString); | |
| throw new Error(errorString); | |
| } | |
| }); | |
| json.messages.forEach((message) => { | |
| if (message.attestation === 'PENDING') { | |
| const errorString = message.delayReason | |
| ? `CCTP attestation is pending due to ${message.delayReason}` | |
| : 'CCTP attestation is still pending'; | |
| switch (message.delayReason) { | |
| case 'insufficient_fee': | |
| case 'amount_above_max': | |
| case 'insufficient_allowance_available': | |
| PrometheusMetrics.logUnhandledError(this.serviceName); | |
| break; | |
| } | |
| logger.error(context, errorString); | |
| throw new Error(errorString); | |
| } | |
| }); |
🤖 Prompt for AI Agents
In typescript/ccip-server/src/services/CCTPAttestationService.ts around lines
166-178, the error string uses message.delayReason directly which can be
undefined and produce "due to undefined"; update the code to normalize the
delayReason (e.g. const reason = message.delayReason ?? 'unknown' or 'not
provided') and use that normalized value in the errorString and log, keep the
existing PrometheusMetrics logging for the specific known delay reasons, and
ensure logs/errors remain descriptive by including the fallback reason when
delayReason is missing.
| const responseJson = await res.json(); | ||
| if (!res.ok) { | ||
| this.core.logger.warn( | ||
| `Server at ${url} responded with error: ${responseJson.error}`, | ||
| ); | ||
| // try next URL | ||
| continue; | ||
| } else { | ||
| return ensure0x(responseJson.data); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mind the mud when parsing responses.
If a server answers with plain text or malformed JSON, res.json() will throw before we can slog on to the next URL, recreating the opaque failure we’re trying to fix. Please catch that parse error and keep looping so we always have a fallback path.
- const responseJson = await res.json();
- if (!res.ok) {
- this.core.logger.warn(
- `Server at ${url} responded with error: ${responseJson.error}`,
- );
- // try next URL
- continue;
- } else {
- return ensure0x(responseJson.data);
- }
+ let responseJson: any;
+ try {
+ responseJson = await res.json();
+ } catch (error) {
+ this.core.logger.warn(
+ `Server at ${url} returned non-JSON payload: ${error}`,
+ );
+ continue;
+ }
+ if (!res.ok) {
+ this.core.logger.warn(
+ `Server at ${url} responded with error: ${responseJson?.error}`,
+ );
+ continue;
+ }
+ return ensure0x(responseJson.data);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const responseJson = await res.json(); | |
| if (!res.ok) { | |
| this.core.logger.warn( | |
| `Server at ${url} responded with error: ${responseJson.error}`, | |
| ); | |
| // try next URL | |
| continue; | |
| } else { | |
| return ensure0x(responseJson.data); | |
| } | |
| let responseJson: any; | |
| try { | |
| responseJson = await res.json(); | |
| } catch (error) { | |
| this.core.logger.warn( | |
| `Server at ${url} returned non-JSON payload: ${error}`, | |
| ); | |
| continue; | |
| } | |
| if (!res.ok) { | |
| this.core.logger.warn( | |
| `Server at ${url} responded with error: ${responseJson?.error}`, | |
| ); | |
| continue; | |
| } | |
| return ensure0x(responseJson.data); |
🤖 Prompt for AI Agents
In typescript/sdk/src/ism/metadata/ccipread.ts around lines 90 to 99, calling
await res.json() can throw on plain-text or malformed responses which breaks the
retry loop; wrap the JSON parse in a try/catch, on parse failure read res.text()
(or use a safe parse) and log a warning containing the URL and raw text/error,
then continue to the next URL instead of letting the exception bubble up; keep
the existing behavior of returning ensure0x(responseJson.data) only when parsing
succeeds and res.ok is true.
Description
Drive-by changes
Related issues
Fixes opaque error
Backward compatibility
Testing
Manual
$ yarn hyperlane status --private-key $(hypkey) --origin base --relay --dispatchTx 0xf78db32e5c77317a25b94608016ea19e4f22aaf337b1ab39ae6052d0d967c9a1 Hyperlane CLI Checking status of message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9 on hyperevm Message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9 was not yet delivered Preparing to relay message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9 Server http://localhost:3000/cctp/getCctpAttestation responded with error: CCTP attestation is pending due to insufficient_fee Failed to relay message 0x6a1e2bbf3e46e356157f7568500429e2f789300fa309add0afc746fe7216fec9, Error: Could not fetch CCIP-read metadataSummary by CodeRabbit
Bug Fixes
Refactor